#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <fstream>
#include <vector>
#include <sstream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unordered_map>

#include "Socket.hpp"
#include "Log.hpp"

const std::string wwwroot = "./wwwroot"; // web根目录
// 配置文件里面就是一堆的路径，服务开始时以配置文件来初始化根目录
const std::string sep = "\r\n";
const std::string homepage = "index.html";

static const int defaultport = 8080;

class HttpServer;

class ThreadData
{
public:
    ThreadData(int fd, HttpServer *s)
        : sockfd(fd), svr(s)
    {
    }
    int sockfd;
    HttpServer *svr;
};

class HttpRequest
{
public:
    void Deserialize(std::string req) // 反序列化，其实就是做字符串切割，把一个http请求字符串变成多个字符串并存起来
    {
        while (true)
        {
            size_t pos = req.find(sep); //找换行符
            if (pos == std::string::npos)
                break; // 找到结尾了，代表全部截完了
            // 开始截取字符串
            std::string temp = req.substr(0, pos);
            if (temp.empty())
                break;                      // 碰到了空行，代表当前报文的报头反序列化完成
            req_header.push_back(temp);     // 把每一个截取到的数据搞到vector里面去
            req.erase(0, pos + sep.size()); // 把原来http第一行的内容去掉，这样下次再找的时候就找第二行，这样找一行移一行，就能在遇到空行前把数据全搞到数组里面去
        }
        text = req;
    }
    void Parse() // 解析
    {
        std::stringstream ss(req_header[0]); // req_header数组保存着第一行字符串，使用stringstream需要以空格为分隔符，以流的方式拆成4个字符串
        ss >> method >> url >> http_version; // 这样就分开了，而且顺序不能改，这是http的规定
        file_path = wwwroot;
        // 我们需要保证开始访问的路径是从web根目录开始的
        if (url == "/" || url == "/index.html") // 如果请求的是首页
        {
            file_path += "/";
            file_path += homepage; // 最后变成 ./wwwroot/index.html
        }
        else
        {
            file_path += url; //  /a/b/c/d.html -> ./wwwroot/a/b/c/d.html
        }
        auto pos = file_path.rfind("."); // 从后往前找点，截取文件后缀
        if (pos == std::string::npos)
        {
            suffix = ".html";
        }
        else
        {
            suffix = file_path.substr(pos); // 从点的位置往后截取直到结尾，拿到文件后缀
        }
    }
    // 测试打印反序列化结果
    void DebugPrint()
    {
        for (auto &line : req_header)
        {
            std::cout << line << std::endl;
            std::cout << "---------------------" << std::endl;
        }
        std::cout << "method: " << method << std::endl;
        std::cout << "url: " << url << std::endl;
        std::cout << "http_version: " << http_version << std::endl;
        std::cout << "file_path: " << file_path << std::endl;
        std::cout << text << std::endl;
    }

public:
    std::vector<std::string> req_header; // 请求行
    std::string text;                    // 请求正文

    // 存储解析之后的结果
    std::string method; //请求得方法
    std::string url; //请求的url
    std::string http_version; // 请求的http版本
    std::string file_path;
    std::string suffix; // 文件后缀
};

class HttpServer
{
public:
    HttpServer(uint16_t port = defaultport)
        : _port(port)
    {
        content_type.insert({".html", "text/html"}); // 初始化，有很多，这里只插入这两个哈
        content_type.insert({".png", "image/png"});  // 如果有需求就通过配置文件去搞
        //如果要返回更多形式的文件，比如视频，就继续加
    }
    ~HttpServer()
    {
    }
    bool Start()
    {
        _listensock.Socket();    // 创建套接字
        _listensock.Bind(_port); // 绑定套接字
        _listensock.Listen();    // 监听套接字
        while (true)
        {
            std::string clientip;
            uint16_t clientport;
            int sockfd = _listensock.Accept(&clientip, &clientport); // 获取连接
            if (sockfd < 0)
                continue;
            pthread_t tid;
            ThreadData *td = new ThreadData(sockfd, this);
            pthread_create(&tid, nullptr, ThreadRun, td);
        }
    }

    static std::string ReadHtmlContent(const std::string &htmlpath)
    {
        std::ifstream in(htmlpath, std::ios::binary); // 以二进制方式来读
        if (!in.is_open())
            return "";
        // 以字符串方式读，传文本还好，传图片视频等二进制数据时，就不行了，所以要在上面ifstream的第二个参数带上std::ios::binary，表示以二进制方式来读
        // std::string line;
        // std::string content;
        // while (std::getline(in, line)) // 第一个是流，第二个是string，从流读到line里面
        // {
        // content += line;
        // }
        // C++读取二进制大小
        //读取前需要知道文件的大小，先把文件读写位置放到结尾，就可以得到大小，然后再把文件读写位置放到开始
        in.seekg(0, std::ios_base::end); // 将文件读写开始位置定位到结尾
        auto len = in.tellg();           // 返回文件大小
        in.seekg(0, std::ios_base::beg); // 再把文件开始位置返回开头

        std::string content;
        content.resize(len); //把缓冲区大小调整为文件的大小

        in.read((char *)content.c_str(), content.size()); // 二进制读

        in.close();
        return content;
    }

    std::string SuffixToDesc(const std::string &suffix) // 把读取到的文件后缀通过映射转化成Content-type所需要的参数
    {
        auto iter = content_type.find(suffix);
        if (iter == content_type.end())
        {
            return content_type[".html"]; // 没找到默认返回 text/html
        }
        else
        {
            return content_type[suffix]; // 返回映射后的参数
        }
    }

    void HandlerHttp(int sockfd) //构建响应，发回去浏览器
    {
        char buffer[10240];
        // man 2 recv 也是可以进行Tcp套接字读取的，它的返回值和使用方法和read几乎一模一样，recv比read多了个参数，flags，标识读取方式，当flags为0时，它的作用就和read一样了
        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = 0;
            // 假设我们读取到的就是一个完整的，独立的http请求
            std::cout << buffer << std::endl;
            // 读到什么内容就打印什么内容
            HttpRequest req;
            req.Deserialize(buffer); //解析有效载荷
            req.Parse();
            // req.DebugPrint();

            // 读取成功之后，返回响应
            std::string text;
            bool ok = true;
            text = ReadHtmlContent(req.file_path); // 将指定路径下的文件的内容返回给text
            if (text.empty())                      // 如果读取的内容不存在，就返回一个404页面
            {
                ok = false;
                std::string err_html = wwwroot;
                err_html += "/";
                err_html += "err.html";

                text = ReadHtmlContent(err_html);
            }
            std::string response_line;
            if (ok)
            {
                response_line = "HTTP/1.0 200 OK\r\n"; //①添加响应行
            }
            else
            {
                response_line = "HTTP/1.0 404 Not Found\r\n";
            }
            // response_line = "HTTP/1.0 302 Found\r\n"; //当用户访问一个页面时，但是可能这个页面已经弃用，所以使用302状态码。就可以直接跳转到其它的指定地址
            std::string response_header = "Content-Length: "; // ②添加响应报头
            response_header += std::to_string(text.size());   // 报头添加正文的长度
            response_header += "\r\n";
            response_header += "Content-Type: "; //图片是二进制的，就是这个响应报文的有效载荷本身是二进制的，所以我们需要告诉浏览器，这个二进制是什么类型的数据，就通过这个参数来告诉浏览器
            response_header += SuffixToDesc(req.suffix); // 根据后缀转化为该属性的内容，使浏览器能识别其它后缀的文件
            response_header += "\r\n";
            response_header += "Set-Cookie: name=haha&&passwd=12345";
            response_header += "\r\n";

            //response_header += "Location: https://www.baidu.com\r\n";

            std::string response = response_line;
            response += response_header; //加报头
            response += sep;
            response += text; // 加有效载荷
            //最后到这里后就是一个完整的报文了

            // 把消息发回去 man 2 send
            send(sockfd, response.c_str(), response.size(), 0);
        }
        close(sockfd);
    }

    static void *ThreadRun(void *args)
    {
        pthread_detach(pthread_self());                   // 线程分离
        ThreadData *td = static_cast<ThreadData *>(args); // 表示把args的类型强转为THreadData
        td->svr->HandlerHttp(td->sockfd); //将响应发回浏览器
        delete td;
        return nullptr;
    }

private:
    Sock _listensock;
    uint16_t _port;
    std::unordered_map<std::string, std::string> content_type;
};
